//
//  Message.m
//  MailDelivery
//
//  Created by Dante Palacios on 30/08/09.
//  Copyright 2009 Dante Palacios. All rights reserved.
//


#import <Security/Security.h>

#import "Message.h"
#import "SMTPMailDelivery.h"

#import "NSDictionaryAdditions.h"
#import "NSStringAdditions.h"
#import "NSAttributedStringAdditions.h"
#import "NSDataAdditions.h"

#import "Errors.h"
#import "Constants.h"
#import "Funtions.h"
#import "Macros.h"

@interface Message ()

@property (readwrite, copy) NSString *identifier;
@property (readwrite, retain) NSDate *creationDate;

@end

@implementation Message


@synthesize representedObject = _representedObject;
@synthesize sender = _sender;
@synthesize toRecipients = _toRecipients;
@synthesize ccRecipients = _ccRecipients;
@synthesize bccRecipients = _bccRecipients;
@synthesize replyTo = _replyTo;
@synthesize subject = _subject;
@synthesize creationDate = _creationDate;
@synthesize dateSent = _dateSent;
@synthesize parts = _parts;
@synthesize source = _source;
@synthesize attachments = _attachments;
@synthesize identifier = _identifier;
@synthesize recipients = _recipients;
@synthesize encoding = _encoding;
@synthesize priority = _priority;


#pragma mark Life Cycle

- (id)init 
{
	self = [super init];
	if (self) 
	{
		self.format = kMultipartAlternativeMessageFormat;
		self.creationDate = [NSDate date];
		self.encoding = NSASCIIStringEncoding;
		self.priority = MessagePriorityNormal;
	}
	return self;
}

- (id)initWithHeaders:(NSDictionary *)messageHeaders content:(NSAttributedString *)messageBody format:(NSString *)messageFormat 
{
	Message *message = [self init];
	if (message) 
	{
		message.headers = messageHeaders;
		message.content = messageBody;
		message.format = messageFormat;
	}
	return message;
}

- (id)initWithHeaders:(NSDictionary *)messageHeaders content:(NSAttributedString *)messageBody 
{
	return [self initWithHeaders:messageHeaders content:messageBody format:kMultipartAlternativeMessageFormat];
}

- (id)copyWithZone:(NSZone *)zone 
{
	Message *message = [super copyWithZone:zone];
	message.representedObject = self.representedObject;
	message.creationDate = [NSDate date];
	message.encoding = self.encoding;
	message.priority = self.priority;
	
	// no, no, no
	//message.identifier = self.identifier;
	//message.parts = self.parts;
	//message.source = self.source;
	return message;
}

- (void)encodeWithCoder:(NSCoder *)aCoder 
{
	[super encodeWithCoder:aCoder];
	
	if ([self.representedObject conformsToProtocol:@protocol(NSCoding)]) 
	{
		[aCoder encodeObject:self.representedObject forKey:@"representedObject"];
	}
	
	[aCoder encodeObject:self.identifier forKey:@"identifier"];
	[aCoder encodeObject:self.parts forKey:@"parts"];
	[aCoder encodeObject:self.source forKey:@"source"];
	[aCoder encodeInteger:self.encoding forKey:@"encoding"];
}

- (id)initWithCoder:(NSCoder *)aDecoder 
{
	self = [super initWithCoder:aDecoder];
	if (self) 
	{
		self.representedObject = [aDecoder decodeObjectForKey:@"representedObject"];
		self.identifier = [aDecoder decodeObjectForKey:@"identifier"];
		self.parts = [aDecoder decodeObjectForKey:@"parts"];
		self.source = [aDecoder decodeObjectForKey:@"source"];
		self.encoding = [aDecoder decodeIntegerForKey:@"encoding"];
	}
	return self;
}

- (void)dealloc 
{
	[_representedObject release]; 
	_representedObject = nil;
	[_identifier release];
	_identifier = nil;
	[_creationDate release];
	_creationDate = nil;
	[_dateSent release]; 
	_dateSent = nil;
	[_parts release];
	_parts = nil;
	[_source release];
	_source = nil;
	[_attachments release];
	_attachments = nil;
	[_sender release];
	_sender = nil;
	[_subject release];
	_subject = nil;
	[_replyTo release];
	_replyTo = nil;
	[_recipients release];
	_recipients = nil;
	[_toRecipients release];
	_toRecipients = nil;
	[_ccRecipients release];
	_ccRecipients = nil;
	[_bccRecipients release];
	_bccRecipients = nil;
	
	[super dealloc];
}

#pragma mark getters & setters

- (NSDate *)creationDate 
{
    return AtomicAutoreleasedGet(_creationDate);
}

- (void)setCreationDate:(NSDate *)value 
{
    AtomicRetainedSet(_creationDate, value);
}

- (NSDate *)dateSent
{
	return AtomicAutoreleasedGet(_dateSent);
}

- (void)setDateSent:(NSDate *)value
{
	AtomicRetainedSet(_dateSent, value);
}

- (NSArray *)attachments
{
	NSArray *value = nil;
	@synchronized(self)
	{
		if (!_attachments)
		{
			_attachments = [[(NSAttributedString *) self.content attachments] retain];
		}
		value = [[_attachments retain] autorelease];
	}
	return value;
}

- (void)setAttachments:(NSArray *)value
{
     AtomicRetainedSet(_attachments, value);
}

- (NSArray *)parts
{
	NSArray *value = nil;
	
	@synchronized(self)
	{
		if (!_parts)
		{
			NSMutableArray *messageParts = [NSMutableArray array];
			
			NSString *plainText = [[(NSAttributedString *) self.content plainText] stringWithCRLFLineBreaks];
			
			if (self.encoding == NSASCIIStringEncoding)
			{
				self.encoding = [NSString encodingForString:plainText];
				self.charset = [NSString mimeNameOfStringEncoding:self.encoding];
			}
			
			self.contentTransferEncoding = (self.encoding != NSASCIIStringEncoding) ? @"8bit" : @"7bit" ;
			
			Part *textPart = [[Part alloc] init];
			textPart.contentType = kPlainTextMessageFormat;
			textPart.charset = self.charset;
			textPart.format = @"flowed";
			textPart.contentTransferEncoding = self.contentTransferEncoding;
			textPart.content = plainText;
			textPart.boundary = self.boundary;
			
			[messageParts addObject:textPart];
			
			[textPart release];
			
			BOOL isMIME = (self.format == kMultipartAlternativeMessageFormat);
			if (isMIME)
			{
				NSString *relatedBoundary = [self relatedBoundary];
				NSString *htmlContent = [[(NSAttributedString *)self.content HTMLString] stringWithCRLFLineBreaks];
				NSMutableArray *attachmentsParts = [NSMutableArray array];
				NSMutableDictionary *attachmentsCIDs = [NSMutableDictionary dictionary];
				
				for (NSTextAttachment *attachment in self.attachments)
				{
					NSFileWrapper *fileWrapper = [attachment fileWrapper];
					
					if (!fileWrapper) continue;
					
					NSString *fileName = [fileWrapper filename];
					if (!fileName)
					{
						fileName = [fileWrapper preferredFilename];
					}
					
					if (!fileName) continue;
					
					NSData *fileData = [fileWrapper regularFileContents];
					
					if (!fileData) continue;
					
					NSString *mimeType = MIMETypeOfFile(fileName);
					
					if (!mimeType) continue;
					
					NSString *attachmentCID = [NSString stringWithFormat:@"%@/%@", [NSString stringWithUUID], [fileName stringByReplacingOccurrencesOfString:@" " withString:@"_"]];
					
					Part *attachmentPart = [[Part alloc] init];
					attachmentPart.content = fileData;
					attachmentPart.contentType = mimeType;
					attachmentPart.contentID = attachmentCID;
					attachmentPart.boundary = relatedBoundary;
					attachmentPart.contentDisposition = IsImageFile(fileName) ? kPartContentDispositionInline : kPartContentDispositionAttachment;
					attachmentPart.filename = fileName;
					attachmentPart.contentTransferEncoding = kPartContentTransferEncodingBase64;
					
					[attachmentsParts addObject:attachmentPart];
					
					[attachmentPart release];
					
					[attachmentsCIDs setObject:attachmentCID forKey:fileName];
					
				}
				
				if (attachmentsCIDs.count)
				{
					NSMutableString *htmlString = [NSMutableString stringWithString:htmlContent];
					NSArray *allKeys = [attachmentsCIDs allKeys];
					for (NSString *fileNameKey in allKeys)
					{
						NSStringEncoding encoding = fileNameKey.encoding;
						NSString *CIDString = [attachmentsCIDs objectForKey:fileNameKey];
						NSString *stringToReplace = [NSString stringWithFormat:@"file:///%@", [fileNameKey stringByAddingPercentEscapesUsingEncoding:encoding]];
						NSString *stringToInsert = [NSString stringWithFormat:@"cid:%@", CIDString];
						
						[htmlString replaceOccurrencesOfString:stringToReplace 
													withString:stringToInsert
													   options:NSLiteralSearch 
														 range:NSMakeRange(0, [htmlString length])];
					}
										
					htmlContent = htmlString;
				}
				
				Part *htmlPart = [[Part alloc] init];
				htmlPart.contentType = kHTMLMessageFormat;
				htmlPart.charset = self.charset;
				htmlPart.contentTransferEncoding = self.contentTransferEncoding;
				htmlPart.content = htmlContent;
				htmlPart.boundary = attachmentsParts.count ? relatedBoundary : self.boundary;
				
				[messageParts addObject:htmlPart];
				[messageParts addObjectsFromArray:attachmentsParts];
				
				[htmlPart release];
			}
			
			_parts = [messageParts copy];			
		}
		
		value = [[_parts copy] autorelease];
	}	
	
	return value;
}

- (void)setParts:(NSArray *)value
{
    AtomicCopiedSet(_parts, value);
}

- (NSString *)source
{
	NSString *value = nil;
	
	@synchronized(self)
	{
		if (!_source)
		{
			// start self part (Message Part)
			
			self.contentTransferEncoding = (self.encoding != NSASCIIStringEncoding) ? @"8bit" : @"7bit" ;
			self.contentType = self.format;
			self.charset = [NSString mimeNameOfStringEncoding:self.encoding];
			
			NSMutableString *messagePart = [NSMutableString string];
			[messagePart appendFormat:@"Message-id: %@\r\n", self.identifier];
			
			BOOL isMIME = (self.format == kMultipartAlternativeMessageFormat);
			
			if (isMIME)
				[messagePart appendFormat:@"Content-Type: %@; boundary=%@\r\n", self.contentType, self.boundary];
			else
				[messagePart appendFormat:@"Content-Type: %@; charset=%@; format=flowed\r\n", self.contentType, self.charset];
			
			NSString *date = [[NSCalendarDate calendarDate] descriptionWithCalendarFormat:@"%a, %e %b %Y %H:%M:%S %z" locale:[[NSUserDefaults standardUserDefaults] dictionaryRepresentation]];
			
			NSBundle *framework = [NSBundle bundleForClass:[Message class]];
			[messagePart appendFormat:@"Date: %@\r\n", date];
			[messagePart appendFormat:@"Mime-Version: 1.0 (%@ framework v%@)\r\n", [framework objectForInfoDictionaryKey:(NSString *) kCFBundleNameKey], [framework objectForInfoDictionaryKey:(NSString *) kCFBundleVersionKey]];
			
			if (/*self.priority && (self.priority != MessagePriorityNormal)*/YES) {
				[messagePart appendFormat:@"X-Priority: %d\r\n", self.priority];
			}
			
			NSArray *keys = [[self.headers allKeys] sortedArrayUsingSelector:@selector(compare:)];
			for (NSString *key in keys) {
				if (!key.length) continue;
				
				NSString *headerName = [key capitalizedString];
				//the value of a header must be a NSString
				NSString *headerValue = nil;
				
				id object = [self.headers objectForKey:key];
				if ([object isKindOfClass:[NSArray class]]) 
				{
					// this works with objects conforming MessageAddressee protocol
					headerValue = [(NSArray *)object componentsJoinedByString:@", "];
				}
				else if ([object isKindOfClass:[NSString class]]) {
					headerValue = (NSString *)object;
				}
				else if ([object isKindOfClass:[MessageAddressee class]] || 
						  [object conformsToProtocol:@protocol(MessageAddressee)]) {
					headerValue = [object fullEmailAddress];//[object description];
				}
				else if ([object isKindOfClass:[NSNumber class]]) {
					headerValue = [object stringValue];
				}
				
				if (![[headerValue stringByTrimmingWhiteSpaces] length]) continue;
				
				//TODO: Check message header line length limits...
				[messagePart appendFormat:@"%@: %@\r\n", headerName, headerValue];
			}
			
			[messagePart appendString:@"\r\n"];
			
			//end self part, append parts
			
			NSMutableString *messageContent = [NSMutableString string];
			if (isMIME) {
				BOOL hasAttachments = (self.parts.count > 2);
				
				for (Part *part in self.parts) {
					id partContent = part.content;
					NSString *contentType = part.contentType;
					if ([partContent isKindOfClass:[NSString class]]) 
					{
						if ((contentType == kHTMLMessageFormat) && hasAttachments) 
						{
							[messageContent appendFormat:@"\r\n--%@\r\n", self.boundary];
							[messageContent appendFormat:@"Content-Type: multipart/related;\r\n\tboundary=%@;\r\n\ttype=\"%@\"\r\n", part.boundary, kHTMLMessageFormat];
						}			
						[messageContent appendFormat:@"\r\n--%@\r\n", part.boundary];
						[messageContent appendFormat:@"Content-Type: %@;\r\n\tcharset=%@;\r\n", part.contentType, part.charset];
						[messageContent appendFormat:@"Content-Transfer-Encoding: %@\r\n", part.contentTransferEncoding];
						[messageContent appendString:@"\r\n"];
						[messageContent appendString:[(NSString *) partContent stringWithCRLFLineBreaks]];
						
					}
					else if ([partContent isKindOfClass:[NSData class]]) 
					{
						[messageContent appendFormat:@"\r\n--%@\r\n", part.boundary];
						[messageContent appendFormat:@"Content-Transfer-Encoding: %@\r\n", part.contentTransferEncoding];
						
						if ([part.contentDisposition isEqualToString:kPartContentDispositionAttachment]) 
						{
							// this content could be displayed inline…
							if ((contentType == kHTMLMessageFormat) || (contentType == kPlainTextMessageFormat)) 
							{
								contentType = @"text/directory";
							}
						}
						
						[messageContent appendFormat:@"Content-Type: %@;\r\n\tx-unix-mode=0644;\r\n\tname=\"%@\"\r\n", contentType, part.filename];
						[messageContent appendFormat:@"Content-Disposition: %@;\r\n\tfilename=%@\r\n", part.contentDisposition, part.filename];
						[messageContent appendFormat:@"Content-Id: <%@>\r\n", part.contentID];
						[messageContent appendString:@"\r\n"];
						
						//every line must end with CRFL line breaks
						[messageContent appendString:[[(NSData *) part.content encodeBase64WithNewlines:YES] stringWithCRLFLineBreaks]];
						[messageContent appendString:@"\r\n"];
					}
				}
				
				if (hasAttachments)
				{
					[messageContent appendFormat:@"\r\n--%@--\r\n", [self relatedBoundary]];
				}
				
				[messageContent appendFormat:@"\r\n--%@--\r\n", self.boundary];
			}
			else 
			{
				[messageContent appendString:[[(NSAttributedString *) self.content plainText] stringWithCRLFLineBreaks]];
			}
			
			// the end
			
			NSMutableString *messageSource = [NSMutableString string];
			[messageSource appendString:messagePart];
			[messageSource appendString:messageContent];
			[messageSource appendString:@"\r\n.\r\n"];
			
			_source = [messageSource copy];
		}
		
		value = [[_source copy] autorelease];
	}
	
	return value;
}

- (void)setSource:(NSString *)value 
{
    AtomicCopiedSet(_source, value);
}

- (NSString *)identifier 
{
	NSString *value = nil;
	@synchronized(self) 
	{
		if (!_identifier) 
		{
			_identifier = [[NSString stringWithUUID] copy];
		}
		value = [[_identifier copy] autorelease];
	}
	
	return value;
}

- (void)setIdentifier:(NSString *)value 
{
    AtomicCopiedSet(_identifier, value);
}

- (void)setHeaders:(NSDictionary *)value
{
	[super setHeaders:value];
	
	@synchronized(self)
	{
		// these values are taken from headers
		[_sender release];
		_sender = nil;
		[_subject release];
		_subject = nil;
		[_replyTo release];
		_replyTo = nil;
		[_recipients release];
		_recipients = nil;
		[_toRecipients release];
		_toRecipients = nil;
		[_ccRecipients release];
		_ccRecipients = nil;
		[_bccRecipients release];
		_bccRecipients = nil;
		
		// reset the source
		[_source release];
		_source = nil;
	}
}

- (id)representedObject
{
    return AtomicAutoreleasedGet(_representedObject);
}

- (void)setRepresentedObject:(id)value
{
    AtomicRetainedSet(_representedObject, value);
}

- (id /*<MessageAddressee>*/)sender
{
	id value = nil;
	@synchronized(self)
	{
		if (!_sender)
		{
			_sender = [[self.headers objectForCaseInsensitiveKey:@"From"] copy];
		}
		
		value = [[_sender copy] autorelease];
	}
	
	return value;
}

- (NSString *)subject
{
	NSString *value = nil;
	@synchronized(self)
	{
		if (!_subject)
		{
			_subject = [[self.headers objectForCaseInsensitiveKey:@"Subject"] retain];
		}
		
		value = [[_subject retain] autorelease];
	}
	
	return value;
}

- (NSArray *)replyTo 
{
    NSArray *value = nil;
	@synchronized(self)
	{
		if (!_replyTo)
		{
			_replyTo = [[self.headers adresseesForMessageHeader:@"Reply-To"] retain];
		}
		
		value = [[_replyTo retain] autorelease];
	}
	
	return value;
}

- (NSArray *)toRecipients
{
	NSArray *value = nil;
	@synchronized(self)
	{
		if (!_toRecipients)
		{
			_toRecipients = [[self.headers adresseesForMessageHeader:@"To"] retain];
		}
		
		value = [[_toRecipients retain] autorelease];
	}
	
	return value;
}

- (NSArray *)ccRecipients
{
	NSArray *value = nil;
	@synchronized(self)
	{
		if (!_ccRecipients)
		{
			_ccRecipients = [[self.headers adresseesForMessageHeader:@"Cc"] retain];
		}
		
		value = [[_ccRecipients retain] autorelease];
	}
	
	return value;
}

- (NSArray *)bccRecipients
{
	NSArray *value = nil;
	@synchronized(self)
	{
		if (!_bccRecipients)
		{
			_bccRecipients = [[self.headers adresseesForMessageHeader:@"Bcc"] retain];
		}
		
		value = [[_bccRecipients retain] autorelease];
	}
	
	return value;
}

- (NSArray *)recipients
{
	NSArray *value = nil;
	@synchronized(self)
	{
		if (!_recipients)
		{
			NSMutableArray *array = [NSMutableArray array];
			[array addObjectsFromArray:self.ccRecipients];
			[array addObjectsFromArray:self.toRecipients];
			[array addObjectsFromArray:self.bccRecipients];
			
			_recipients = [array retain];
		}
		
		value = [[_recipients retain] autorelease];
	}
	
	return value;
}

#pragma mark private methods

- (NSString *)relatedBoundary;
{
	NSString *boundary = self.boundary;
	NSString *baseBoundary = [[boundary componentsSeparatedByString:@"--"] lastObject];
	NSArray *boundaryItems = [boundary componentsSeparatedByString:@"-"];
	NSString *bundleName = [boundaryItems objectAtIndex:0];
	NSInteger partNumber = [[boundaryItems objectAtIndex:1] integerValue];
	return [NSString stringWithFormat:@"%@-%d--%@", bundleName, (partNumber - 1), baseBoundary];
}

@end